본문 바로가기

iOS개발팁

ObjC 객체의 retainCount 이야기

어제 적었던 ‘아리송한 ObjC의 레퍼런스 카운트’ 글에 이어 ObjC 객체의 참조 횟수 이야기를 좀 더 해보려고 합니다. retainCount 이슈를 좀 더 조사해 보니 ObjC에서 객체를 alloc, init- 으로 생성했다고 해서 객체의 retainCount가 1로 시작하는 것은 아니었습니다. 그 이유는 성능상의 최적화를 위해서 그리고 힙에 생성되는 객체와 스택에 생성되는 객체를 구분하기 위함입니다. 우선 성능상 최적화를 위한 예를 보겠습니다.

1. 성능최적화와 NSNumber의 retainCount
아래 코드의 NSNumber 객체 포인터인 n의 참조횟수는 몇 일까요?
NSNumber* n = [[NSNumber alloc] initWithInt:100];
NSLog(@"Count of n : %i", [n retainCount]);
결과는 아래와 같습니다. 생각대로 1이 나왔습니다.
2010-07-16 11:26:34.463 Count of n : 1
하지만 아래처럼한다면 어떻게 될까요?
NSNumber* n = [[NSNumber alloc] initWithInt:1];
NSLog(@"Count of n : %i", [n retainCount]);
위의 코드에서 변경된 부분은 100으로 NSNumber를 생성하던 것을 1로 생성한 것입니다. 과연 1이 나올까요? 결과는 아래와 같습니다.
2010-07-16 11:26:34.463 Count of n : 2
어랏? 2가 나왔습니다. 바로 iOS가 성능 최적화를 위해서 일정 범위의 숫자까지는 미리 생성해 캐슁해 놓고 해당 범위의 객체 생성 요청이 있을 경우 참조 횟수만 늘려서 객체의 포인터를 넘겨 주는 것입니다. 그 일정 범위를 알아보기 위해서 아래와 같이 코드를 작성해 보았습니다.
for(int i=0; i<100; i++)
{
     NSNumber* n2 = [[NSNumber alloc] initWithInt:i];
     NSLog(@"[%i] Count of n2: %i",i, [n2 retainCount]);
     [n2 release];
}
결과는 아래와 같습니다.
2010-07-16 11:46:23.874  [0] Count of n2: 2
2010-07-16 11:46:23.874  [1] Count of n2: 2
2010-07-16 11:46:23.875  [2] Count of n2: 2
2010-07-16 11:46:23.875  [3] Count of n2: 2
2010-07-16 11:46:23.876  [4] Count of n2: 2
2010-07-16 11:46:23.876  [5] Count of n2: 2
2010-07-16 11:46:23.876  [6] Count of n2: 2
2010-07-16 11:46:23.876  [7] Count of n2: 2
2010-07-16 11:46:23.877  [8] Count of n2: 2
2010-07-16 11:46:23.877  [9] Count of n2: 2
2010-07-16 11:46:23.877  [10] Count of n2: 2
2010-07-16 11:46:23.878  [11] Count of n2: 2
2010-07-16 11:46:23.878  [12] Count of n2: 2
2010-07-16 11:46:23.878  [13] Count of n2: 1
2010-07-16 11:46:23.879  [14] Count of n2: 1
즉, 0부터 12까지는 성능최적화를 위해서 미리 NSNumber를 생성해 놓는 것입니다. 물론 이 범위는 iOS의 버전 마다 다를 수 있을 것입니다. 따라서 아래와 같이 코드를 작성하면 retainCount가 생각과 달리 나오게 되는 것입니다.
NSNumber* n = [[NSNumber alloc] initWithInt:1];
NSLog(@"Count of n : %i", [n retainCount]);
   
NSNumber* n1 = [[NSNumber alloc] initWithInt:1];
NSLog(@"Count of n1 : %i", [n1 retainCount]);
   
NSNumber* n2 = [[NSNumber alloc] initWithInt:1];
NSLog(@"Count of n2 : %i", [n2 retainCount]);
결과는 아래와 같습니다.
2010-07-16 11:51:12.939  Count of n : 2
2010-07-16 11:51:12.940  Count of n1 : 3
2010-07-16 11:51:12.941  Count of n2 : 4

2. 힙객체와 스택객체의 구분을 위한 retainCount
ObjC에서 모든 객체는 참조 횟수를 통해서 메모리 관리를 합니다. 스택 객체도 그러할텐데 스택에 생성한 객체는 개발자가 따로 release를 하지 않죠. 원래 스택에 생성된 객체는 지역 변수이므로 자신이 속한 블럭이 끝나면 소멸되어야 합니다. 다음 코드를 살펴볼까요?
NSString *myString = @"my string";
NSLog(@"Count of myString: %u", [myString retainCount]);
결과는 아래와 같습니다.
2010-07-16 11:53:36.237 Count of myString3: 4294967295
스택객체의 retainCount는 0xFFFFFFFF로 UINT_MAX값을 갖습니다. 이는 애플의 개발자 문서에도 나와 있는 사항으로 아래와 같이 적혀 있습니다.
For objects that never get released (that is, their release method does nothing), this method should return UINT_MAX, as defined in <limits.h>.
즉, 절대 release되지 말아할 객체는 UINT_MAX를 반환해야 한다고 명시되어 있습니다. 스택 객체와 같이 언어시스템이 관리하는 객체나 Singleton객체 같은 경우 UINT_MAX를 반환하여 개발자가 해당 객체에 release메시지를 보내도 아무런 효과가 없도록 하는 것입니다. 위의 문구에서 처럼 실제로 release 메서드가 아무런 일도 안 하도록 코드를 작성해야 합니다. 스택 객체에게 release메시지를 보내서 해제 되면 큰일이 나겠죠?

하지만 NSString 에서도 약간 아리송한 점이 있습니다. 아래의 코드를 보시죠.
NSString *myString = [[NSString alloc] initWithString:@"my string"];
NSLog(@"Count of myString: %u", [myString retainCount]);
NSString *myString1 = @"my string";
NSLog(@"Count of myString1: %u", [myString1 retainCount]);
힙과 스택에 스트링 객체를 생성하였습니다. 결과는 아래와 같습니다.
2010-07-16 12:04:12.936  Count of myString: 4294967295
2010-07-16 12:08:43.238  Count of myString1: 4294967295
즉, initWithString초기자는 스택객체로 만들어 버립니다. 하지만 initWithFormat으로 만들면 힙객체가 됩니다.
NSString *myString2= [[NSString alloc] initWithFormat:@"%@", @"my string"];
NSLog(@"Count of myString2: %u", [myString2 retainCount]);
결과는 아래와 같습니다.
2010-07-16 12:06:43.452  Count of myString2: 1
힙객체를 사용해서 iniWithString초기자를 이용해 생성해도 아래와 같이 스택 객체로 생성합니다.
NSString *myString3 = [[NSString alloc] initWithString:myString2];
NSLog(@"Count of myString4: %u", [myString3 retainCount]);
2010-07-16 12:51:14.793 Count of myString3: 4294967295
이 부분은 성능최적화를 위한 것인지 아니면 다른 이유로 그런 것인지 모르겠지만 지금까지의 이야기를 바탕으로 중요한 결론을 얻을 수 있습니다.

3. 결론
중요한 결론은 retainCount를 프로그램의 로직에 포함하여 사용해서는 안된다는 것입니다. 예를 들어 아래와 같은 코드는 문제가 될 수 있는 코드입니다.
if([myString retainCount] > 0)
        [myString release];
단지 retainCount는 디버깅시에 정보를 얻기 위함으로 사용해야 합니다. retainCount에 대한 좀 더 자세한 이야기를 알게되면 다시 글을 적겠습니다 ;) 해피코딩하세요^^

'iOS개발팁' 카테고리의 다른 글

아이폰 OpenGL ES 튜토리얼 01  (9) 2010.07.19
아이폰 OpenGL ES 튜토리얼 시작  (0) 2010.07.19
아리송한 ObjC의 레퍼런스 카운트  (2) 2010.07.15
트윗 WWDC10  (0) 2010.06.19
트윗 WWDC10  (0) 2010.06.19